/**
 * Test the user-facing syntax of $setWindowFields. For example:
 * - Which options are allowed?
 * - When is an expression expected, vs a constant?
 * - Which window functions accept bounds?
 * - When something is not allowed, what error message do we expect?
 */

const coll = db.setWindowFields_parse;
coll.drop();

function run(stage, extraCommandArgs = {}) {
    return coll.runCommand(
        Object.merge({aggregate: coll.getName(), pipeline: [stage], cursor: {}}, extraCommandArgs));
}

// Test that the stage spec must be an object.
assert.commandFailedWithCode(run({$setWindowFields: "invalid"}), ErrorCodes.FailedToParse);

// Test that the stage parameters are the correct type.
assert.commandFailedWithCode(run({$setWindowFields: {sortBy: "invalid"}}), ErrorCodes.TypeMismatch);
assert.commandFailedWithCode(run({$setWindowFields: {output: "invalid"}}), ErrorCodes.TypeMismatch);

// Test that parsing fails for an invalid partitionBy expression.
assert.commandFailedWithCode(
    run({$setWindowFields: {partitionBy: {$notAnOperator: 1}, output: {}}}),
    ErrorCodes.InvalidPipelineOperator);

// Since partitionBy can be any expression, it can be a variable.
assert.commandWorked(run({$setWindowFields: {partitionBy: "$$NOW", output: {}}}));
assert.commandWorked(
    run({$setWindowFields: {partitionBy: "$$myobj.a", output: {}}}, {let : {myobj: {a: 456}}}));

// Test that parsing fails for unrecognized parameters.
assert.commandFailedWithCode(run({$setWindowFields: {what_is_this: 1}}),
                             ErrorCodes.IDLUnknownField);

// Test for a successful parse, ignoring the response documents.
assert.commandWorked(run({
    $setWindowFields: {
        partitionBy: "$state",
        sortBy: {city: 1},
        output: {a: {$sum: 1, window: {documents: ["unbounded", "current"]}}}
    }
}));

function runWindowFunction(spec) {
    // Include a single-field sortBy in this helper to allow all kinds of bounds.
    return run({$setWindowFields: {sortBy: {ts: 1}, output: {v: spec}}});
}

// The most basic case: $sum everything.
assert.commandWorked(runWindowFunction({$sum: "$a"}));

// That's equivalent to bounds of [unbounded, unbounded].
assert.commandWorked(
    runWindowFunction({$sum: "$a", window: {documents: ['unbounded', 'unbounded']}}));

// Extra arguments to a window function are rejected.
assert.commandFailedWithCode(runWindowFunction({abcde: 1}),
                             ErrorCodes.FailedToParse,
                             'Window function $sum found an unknown argument: abcde');

// Bounds can be bounded, or bounded on one side.
assert.commandWorked(runWindowFunction({"$sum": "$a", window: {documents: [-2, +4]}}));
assert.commandWorked(runWindowFunction({"$sum": "$a", window: {documents: [-3, 'unbounded']}}));
assert.commandWorked(runWindowFunction({"$sum": "$a", window: {documents: ['unbounded', +5]}}));
assert.commandWorked(runWindowFunction({"$max": "$a", window: {documents: [-3, 'unbounded']}}));

// Range-based bounds:
assert.commandWorked(runWindowFunction({$sum: "$a", window: {range: ['unbounded', 'unbounded']}}));
assert.commandWorked(runWindowFunction({$sum: "$a", window: {range: [-2, +4]}}));
assert.commandWorked(runWindowFunction({$sum: "$a", window: {range: [-3, 'unbounded']}}));
assert.commandWorked(runWindowFunction({$sum: "$a", window: {range: ['unbounded', +5]}}));
assert.commandWorked(
    runWindowFunction({$sum: "$a", window: {range: [NumberDecimal('1.42'), NumberLong(5)]}}));

// Time-based bounds:
assert.commandWorked(
    runWindowFunction({"$sum": "$a", window: {range: [-3, 'unbounded'], unit: 'hour'}}));

// Numeric bounds can be a constant expression:
let expr = {$add: [2, 2]};
assert.commandWorked(runWindowFunction({"$sum": "$a", window: {documents: [expr, expr]}}));
assert.commandWorked(runWindowFunction({"$sum": "$a", window: {range: [expr, expr]}}));
assert.commandWorked(
    runWindowFunction({"$sum": "$a", window: {range: [expr, expr], unit: 'hour'}}));
// But 'current' and 'unbounded' are not expressions: they're more like keywords.
assert.commandFailedWithCode(
    runWindowFunction({"$sum": "$a", window: {documents: [{$const: 'current'}, 3]}}),
    ErrorCodes.FailedToParse,
    'Numeric document-based bounds must be an integer');
assert.commandFailedWithCode(runWindowFunction({"$sum": "$a", range: [{$const: 'current'}, 3]}),
                             ErrorCodes.FailedToParse,
                             'Range-based bounds expression must be a number');

// Bounds must not be backwards.
function badBounds(bounds) {
    assert.commandFailedWithCode(runWindowFunction(Object.merge({"$sum": "$a"}, {window: bounds})),
                                 5339900,
                                 'Lower bound must not exceed upper bound');
}
badBounds({documents: [+1, -1]});
badBounds({range: [+1, -1]});
badBounds({range: [+1, -1], unit: 'day'});

badBounds({documents: ['current', -1]});
badBounds({range: ['current', -1]});
badBounds({range: ['current', -1], unit: 'day'});

badBounds({documents: [+1, 'current']});
badBounds({range: [+1, 'current']});
badBounds({range: [+1, 'current'], unit: 'day'});

// Any bound besides [unbounded, unbounded] requires a sort:
// - document-based
assert.commandWorked(run({
    $setWindowFields:
        {output: {v: {$sum: "$a", window: {documents: ['unbounded', 'unbounded']}}}}
}));
assert.commandFailedWithCode(
    run({
        $setWindowFields:
            {output: {v: {$sum: "$a", window: {documents: ['unbounded', 'current']}}}}
    }),
    5339901,
    'Document-based bounds require a sortBy');
// - range-based
assert.commandFailedWithCode(
    run({
        $setWindowFields: {output: {v: {$sum: "$a", window: {range: ['unbounded', 'unbounded']}}}}
    }),
    5339902,
    'Range-based bounds require sortBy a single field');
assert.commandFailedWithCode(
    run({
        $setWindowFields: {
            sortBy: {a: 1, b: 1},
            output: {v: {$sum: "$a", window: {range: ['unbounded', 'unbounded']}}}
        }
    }),
    5339902,
    'Range-based bounds require sortBy a single field');

assert.commandFailedWithCode(
    run({$setWindowFields: {output: {v: {$sum: "$a", window: {range: ['unbounded', 'current']}}}}}),
    5339902,
    'Range-based bounds require sortBy a single field');
assert.commandFailedWithCode(
    run({
        $setWindowFields: {
            sortBy: {a: 1, b: 1},
            output: {v: {$sum: "$a", window: {range: ['unbounded', 'current']}}}
        }
    }),
    5339902,
    'Range-based bounds require sortBy a single field');
// - time-based
assert.commandFailedWithCode(
    run({
        $setWindowFields:
            {output: {v: {$sum: "$a", window: {range: ['unbounded', 'unbounded'], unit: 'second'}}}}
    }),
    5339902);

assert.commandFailedWithCode(
    run({
        $setWindowFields: {
            sortBy: {a: 1, b: 1},
            output: {v: {$sum: "$a", window: {range: ['unbounded', 'unbounded'], unit: 'second'}}}
        }
    }),
    5339902);
assert.commandFailedWithCode(
    run({
        $setWindowFields:
            {output: {v: {$sum: "$a", window: {range: ['unbounded', 'current'], unit: 'second'}}}}
    }),
    5339902,
    'Range-based bounds require sortBy a single field');
assert.commandFailedWithCode(
    run({
        $setWindowFields: {
            sortBy: {a: 1, b: 1},
            output: {v: {$sum: "$a", window: {range: ['unbounded', 'current'], unit: 'second'}}}
        }
    }),
    5339902,
    'Range-based bounds require sortBy a single field');

// Variety of accumulators:
assert.commandWorked(run({
    $setWindowFields:
        {sortBy: {ts: 1},
         output: {v: {$sum: "$a", window: {documents: ['unbounded', 'current']}}}}
}));
assert.commandWorked(run({
    $setWindowFields:
        {sortBy: {ts: 1},
         output: {v: {$avg: "$a", window: {documents: ['unbounded', 'current']}}}}
}));
assert.commandWorked(run({
    $setWindowFields:
        {sortBy: {ts: 1},
         output: {v: {$max: "$a", window: {documents: ['unbounded', 'current']}}}}
}));
assert.commandWorked(run({
    $setWindowFields:
        {sortBy: {ts: 1},
         output: {v: {$min: "$a", window: {documents: ['unbounded', 'current']}}}}
}));

// Not every accumulator is automatically a window function.
let err = assert.commandFailedWithCode(run({$setWindowFields: {output: {a: {b: {$sum: "$a"}}}}}),
                                       ErrorCodes.FailedToParse);
assert.includes(err.errmsg, 'Expected a $-prefixed window function, b');

err = assert.commandFailedWithCode(
    run({$setWindowFields: {output: {total: {sum: "$x", window: {documents: [-1, 1]}}}}}),
    ErrorCodes.FailedToParse);
assert.includes(err.errmsg, 'Expected a $-prefixed window function, sum');

err = assert.commandFailedWithCode(run({$setWindowFields: {output: {total: {}}}}),
                                   ErrorCodes.FailedToParse);
assert.includes(err.errmsg, 'Expected a $-prefixed window function');

err = assert.commandFailedWithCode(run({$setWindowFields: {output: {v: {$mergeObjects: "$a"}}}}),
                                   ErrorCodes.FailedToParse);
assert.includes(err.errmsg, 'Unrecognized window function, $mergeObjects');

err = assert.commandFailedWithCode(run({$setWindowFields: {output: {v: {$accumulator: "$a"}}}}),
                                   ErrorCodes.FailedToParse);
assert.includes(err.errmsg, 'Unrecognized window function, $accumulator');

err = assert.commandFailedWithCode(
    run({
        $setWindowFields:
            {output: {total: {$summ: "$x", window: {documents: ['unbounded', 'current']}}}}
    }),
    ErrorCodes.FailedToParse);
assert.includes(err.errmsg, 'Unrecognized window function, $summ');

err = assert.commandFailedWithCode(
    run({
        $setWindowFields:
            {output: {total: {$summ: "$x", windoww: {documents: ['unbounded', 'current']}}}}
    }),
    ErrorCodes.FailedToParse);
assert.includes(err.errmsg, 'Unrecognized window function, $summ');

// Test that an empty object is a valid projected field.
assert.commandWorked(coll.insert({}));
assert.commandWorked(run({$setWindowFields: {output: {v: {$max: {mergeObjects: {}}}}}}));

// However conflicting field paths is always an error.
err = assert.commandFailedWithCode(
    run({$setWindowFields: {output: {a: {$sum: 1}, 'a.b': {$sum: 1}}}}), 6307900);
assert.includes(err.errmsg, 'specification contains two conflicting paths');
